﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Data;
using OverStore.Runtime.Storage;
using OverStore.Configuration.Storage;

namespace OverStore.Configuration.Builder.Fluent
{
    /// <summary>
    /// Exposes set of static methods and nested classes
    /// allows to build ADO.NET command with text and parameters.
    /// </summary>
    /// <typeparam name="TNext">The type of fluent step is returned when command is completed.</typeparam>
    public static class CommandBuilder<TNext>
    {
        /// <summary>
        /// 
        /// </summary>
        public class Parameters
        {
            /// <summary>
            /// 
            /// </summary>
            private Func<IComponentConfiguration<ICommand>, TNext> _Complete;

            /// <summary>
            /// Initializes a new instance of the <see cref="CommandBuilder&lt;TNext&gt;.Parameters"/> class.
            /// </summary>
            /// <param name="complete">The complete.</param>
            public Parameters(Func<IComponentConfiguration<ICommand>, TNext> complete)
            {
                if (Object.ReferenceEquals(complete, null))
                    throw new ArgumentNullException("complete");
                CommandParameters = new List<AdoNetParameter>();
                _Complete = complete;
            }

            /// <summary>
            /// Gets or sets the command text.
            /// </summary>
            /// <value>The command text.</value>
            public string CommandText { get; set; }

            /// <summary>
            /// Gets or sets the type of the command.
            /// </summary>
            /// <value>The type of the command.</value>
            public CommandType CommandType { get; set; }

            /// <summary>
            /// Gets or sets the command parameters.
            /// </summary>
            /// <value>The command parameters.</value>
            public List<AdoNetParameter> CommandParameters { get; set; }

            /// <summary>
            /// Gets or sets the type of the result.
            /// </summary>
            /// <value>The type of the result.</value>
            public AdoCommandResultType ResultType { get; set; }

            /// <summary>
            /// Creates the command.
            /// </summary>
            /// <returns></returns>
            public AdoNetCommandConfiguration CreateCommand()
            {
                return new AdoNetCommandConfiguration
                {
                    CommandType = this.CommandType,
                    Parameters = this.CommandParameters,
                    Sql = this.CommandText,
                    ResultType = this.ResultType
                };
            }

            /// <summary>
            /// Completes the specified command.
            /// </summary>
            /// <param name="command">The command.</param>
            /// <returns></returns>
            public TNext Complete(IComponentConfiguration<ICommand> command)
            {
                return _Complete(command);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public class ResultGettingTypeStep
        {
            private readonly Parameters _P;

            /// <summary>
            /// Initializes a new instance of the <see cref="CommandBuilder&lt;TNext&gt;.ResultGettingTypeStep"/> class.
            /// </summary>
            /// <param name="p">The p.</param>
            public ResultGettingTypeStep(Parameters p)
            {
                if (Object.ReferenceEquals(p, null))
                    throw new ArgumentNullException("p");
                _P = p;
            }

            /// <summary>
            /// Executes data reader to read data output by the command.
            /// </summary>
            public CommandTypeAndTextStep ExecuteAsReader()
            {
                _P.ResultType = AdoCommandResultType.DataReaderResultSet;
                return new CommandTypeAndTextStep(_P);
            }

            /// <summary>
            /// Executes scalar and output data from the command using output parameters.
            /// </summary>
            public CommandTypeAndTextStep ExecuteAsCommand()
            {
                _P.ResultType = AdoCommandResultType.OutputParameters;
                return new CommandTypeAndTextStep(_P);
            }

            /// <summary>
            /// Uses the type of the result.
            /// </summary>
            public CommandTypeAndTextStep UseResultType(AdoCommandResultType resultType)
            {
                _P.ResultType = resultType;
                return new CommandTypeAndTextStep(_P);
            }
        }

        /// <summary>
        /// Initializes Command type and text.
        /// </summary>
        public class CommandTypeAndTextStep
        {
            private readonly Parameters _P;

            /// <summary>
            /// Initializes a new instance of the <see cref="CommandBuilder&lt;TNext&gt;.CommandTypeAndTextStep"/> class.
            /// </summary>
            /// <param name="p">The p.</param>
            public CommandTypeAndTextStep(Parameters p)
            {
                _P = p;
            }

            /// <summary>
            /// Configures command to execute specified SQL text.
            /// </summary>
            public CommandParametersStep UseSqlCommand(string sqlCommand)
            {
                if (String.IsNullOrEmpty(sqlCommand))
                    throw new ArgumentNullException("sqlCommand");
                _P.CommandText = sqlCommand;
                _P.CommandType = CommandType.Text;
                return new CommandParametersStep(_P);
            }

            /// <summary>
            /// Uses the stored procedure.
            /// </summary>
            public CommandParametersStep UseStoredProcedure(string spName)
            {
                if (String.IsNullOrEmpty(spName))
                    throw new ArgumentNullException("spName");

                _P.CommandType = CommandType.StoredProcedure;
                _P.CommandText = spName;
                return new CommandParametersStep(_P);
            }
        }

        /// <summary>
        /// Initializes command parameters.
        /// </summary>
        public class CommandParametersStep
        {
            private readonly Parameters _P;

            /// <summary>
            /// Initializes a new instance of the <see cref="CommandBuilder&lt;TNext&gt;.CommandParametersStep"/> class.
            /// </summary>
            /// <param name="p">The p.</param>
            public CommandParametersStep(Parameters p)
            {
                _P = p;
            }

            #region Parameters

            /// <summary>
            /// Adds the specified parameter to parameters collection.
            /// </summary>
            /// <param name="parameter">The parameter to add in parameters collection.</param>
            /// <returns>The <see cref="AdoNetCommandConfiguration"/> instance with added parameter.</returns>
            public CommandParametersStep WithParameter(AdoNetParameter parameter)
            {
                if (parameter == null)
                    throw new ArgumentNullException("parameter");
                _P.CommandParameters.Add(parameter);
                return this;
            }

            /// <summary>
            /// Adds nullable input parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep NullableInputParameter(string name, DbType type)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                var p = new AdoNetParameter
                {
                    AllowNull = true,
                    DataType = type,
                    Direction = ParameterDirection.Input,
                    Name = name
                };
                return WithParameter(p);
            }

            /// <summary>
            /// Adds nullable input parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <param name="errorMessage">String used as message for exception that is thrown if parameter value is missing or invalid.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep NullableInputParameter(string name, DbType type, string errorMessage)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                var p = new AdoNetParameter
                {
                    AllowNull = true,
                    DataType = type,
                    Direction = ParameterDirection.Input,
                    Name = name,
                    ErrorMessage = errorMessage
                };
                return WithParameter(p);
            }

            /// <summary>
            /// Adds non-nullable input parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep InputParameter(string name, DbType type)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = false,
                    DataType = type,
                    Direction = ParameterDirection.Input,
                    Name = name
                });
            }

            /// <summary>
            /// Adds non-nullable input parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <param name="errorMessage">String used as message for exception that is thrown if parameter value is missing or invalid.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep InputParameter(string name, DbType type, string errorMessage)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = false,
                    DataType = type,
                    Direction = ParameterDirection.Input,
                    Name = name,
                    ErrorMessage = errorMessage
                });
            }

            /// <summary>
            /// Adds nullable output parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep NullableOutputParameter(string name, DbType type)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = true,
                    DataType = type,
                    Direction = ParameterDirection.Output,
                    Name = name
                });
            }

            /// <summary>
            /// Adds nullable output parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <param name="errorMessage">String used as message for exception that is thrown if parameter value is missing or invalid.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep NullableOutputParameter(string name, DbType type, string errorMessage)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = true,
                    DataType = type,
                    Direction = ParameterDirection.Output,
                    Name = name,
                    ErrorMessage = errorMessage
                });
            }

            /// <summary>
            /// Adds non-nullable output parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep OutputParameter(string name, DbType type)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = false,
                    DataType = type,
                    Direction = ParameterDirection.Output,
                    Name = name
                });
            }

            /// <summary>
            /// Adds non-nullable output parameter.
            /// </summary>
            /// <param name="name">The name of parameter.</param>
            /// <param name="type">The type of parameter.</param>
            /// <param name="errorMessage">String used as message for exception that is thrown if parameter value is missing or invalid.</param>
            /// <returns>The new instance of <see cref="AdoNetParameter"/> class.</returns>
            public CommandParametersStep OutputParameter(string name, DbType type, string errorMessage)
            {
                if (String.IsNullOrEmpty(name))
                    throw new ArgumentNullException("name");
                return WithParameter(new AdoNetParameter
                {
                    AllowNull = false,
                    DataType = type,
                    Direction = ParameterDirection.Output,
                    Name = name,
                    ErrorMessage = errorMessage
                });
            }

            #endregion

            /// <summary>
            /// Completes command configuration.
            /// </summary>
            public TNext EndCommand()
            {
                return _P.Complete(_P.CreateCommand());
            }

            /// <summary>
            /// After executing first command executes second command with the same parameters.
            /// </summary>
            public CommandBuilder<TNext>.ResultGettingTypeStep ThenExecute(ResultComposition resultComposition)
            {
                return new CommandBuilder<TNext>.ResultGettingTypeStep(
                    new CommandBuilder<TNext>.Parameters(
                        c2 => _P.Complete(new AdoNetScriptConfiguration
                                    {
                                        FirstCommand = _P.CreateCommand(),
                                        SecondCommand = (AdoNetCommandBaseConfiguration)c2,
                                        ResultComposition = resultComposition
                                    })));
            }
        }
    }
}
